perf: reduce Refit.Reflection memory allocations#2267
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2267 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 183 187 +4
Lines 9745 9835 +90
Branches 1867 1895 +28
=========================================
+ Hits 9745 9835 +90 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
glennawatson
force-pushed
the
perf/reflection-allocations
branch
from
July 20, 2026 14:45
03cd0b4 to
7c0d90d
Compare
Cut allocations across the reflection request builder's per-request and per-method-parse paths, behaviour-identical, guided by GcVerbose micro-benchmarks. - Query subsystem: cache per-type query-property metadata and the attribute provider, cache the enumerable classification, drop the iterator, and parse an existing query string span-based without a NameValueCollection - object/collection query flatten down 69-82%. - Object-path / multipart / headers: cache per-parameter attributes, build the route object-property lookup lazily, value-tuple bindings, emit static headers without a per-request copy. - Cold parse: read each parameter's attributes in one GetCustomAttributes pass, cache the declared factory-method lookup (FindDeclaredMethod -> 0 B, BuildRestResultFuncForMethod -76%), lazy/presize the parameter maps. - Add a Refit.Reflection.Benchmarks GcVerbose micro-benchmark project and a Refit.Reflection.Tests project pinning the request-builder contracts; widen the relevant private members to internal (fields stay private) for benchmarking. End-to-end reflection request build is ~21% faster on a query-object method (6.5 -> 5.2 us). No public API change; behaviour and serialised output unchanged.
glennawatson
force-pushed
the
perf/reflection-allocations
branch
from
July 20, 2026 14:46
7c0d90d to
495de36
Compare
ChrisPulman
approved these changes
Jul 20, 2026
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What kind of change does this PR introduce?
Performance / refactor of the Refit.Reflection library (the reflection request builder). No behavioural change and no public API change.
What is the new behavior?
Reduced memory allocations across the reflection request builder's per-request and per-method-parse paths, guided by a new GcVerbose micro-benchmark suite and verified behaviour-identical. Memory was the primary goal (time improvements are a bonus).
NameValueCollection- object/collection query flatten down 69-82%.GetCustomAttributespass, cache the declared factory-method lookup (FindDeclaredMethod-> 0 B,BuildRestResultFuncForMethod-76%), lazy/presize the parameter maps.Refit.Reflection.BenchmarksGcVerbose micro-benchmark project (91 benchmarks) and aRefit.Reflection.Testsproject pinning the request-builder contracts; the relevant private members are widened to internal (fields stay private) so the benchmarks can reach them.What is the current behavior?
The reflection request builder re-read attributes per call, materialised intermediate collections, and re-scanned method metadata, so each request build allocated more than necessary.
What might this PR break?
None. Behaviour and serialised output are unchanged, verified by
Refit.Tests(1152) plus a newRefit.Reflection.Tests(41) pinning the request-builder contracts. No public API changes; the widened members areinternal.Additional information
Component-level allocation reductions (GcVerbose
Allocated, deterministic) - the primary goal:BuildQueryMapObject)AddQueryParametersObject)FindDeclaredMethodBuildRestResultFuncForMethodConstructBuilder(parse all method infos)BuildParameterMapMultiSegmentEnd-to-end request build (public reflection path, net8.0) - the high-level A/B:
Every remaining allocation was trace-verified against its GcVerbose stack and is either the produced output (the request message, its Options/Uri/headers/body content, the built delegate, or stored value-equatable metadata), a framework allocation inside a call the reflection path must make (
System.Reflection/System.Text.Json/System.Uri/HttpClient), or a value-type box the reflection API forces - nothing reducible remains in Refit.Reflection code.